/*
 * JSF Toolkit Component Framework
 * Copyright (C) 2007 Noah Sloan <iamnoah A-T gmail D0T com>
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 */
package com.jsftoolkit.base.renderer;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;

import org.apache.shale.remoting.Mechanism;
import org.apache.shale.remoting.XhtmlHelper;

import com.jsftoolkit.base.ResourceConstants;
import com.jsftoolkit.base.ResourceInfo;
import com.jsftoolkit.base.ResourceInfo.Type;
import com.jsftoolkit.utils.ResourceFilter;
import com.jsftoolkit.utils.Utils;

/**
 * Contains methods for including and filtering component resources.
 * 
 * @author noah
 * 
 */
public class ResourceUtils {

	/**
	 * The attribute on the {@link UIViewRoot} storing the resources that have
	 * already been included.
	 */
	protected static final String RENDERED_RESOURCES = "com.jsftoolkit.ResourcesUtils.rendererResources";

	/**
	 * Suffix appended to the resource id, to create the id for the encoding
	 * attribute.
	 */
	public static final String ENCODING_SUFFIX = ".ENCODING";

	/**
	 * Suffix appended to the resource id, to create the id for the bean that is
	 * the resource translator.
	 */
	public static final String BEAN_SUFFIX = ".BEAN";

	public static final XhtmlHelper HELPER = new XhtmlHelper();

	/**
	 * 
	 * @param resource
	 * @return the type of the resource, guessing it from the filename of it is
	 *         not specified.
	 */
	public static String getType(ResourceInfo resource) {
		if (resource.getResourceType() != null) {
			return resource.getResourceType().toString();
		}
		// if the type is null, guess it from the filename
		return resource.getDefaultResource().endsWith(".css") ? Type.STYLE
				.toString() : Type.SCRIPT.toString();
	}

	/**
	 * Resolves the name of the managed bean to use as the resource filter.
	 * 
	 * @see #getActualValue(String, String, FacesContext, UIComponent)
	 * 
	 * @param resource
	 * @param component
	 * @param context
	 * @return
	 */
	public static String getBean(ResourceInfo resource, UIComponent component,
			FacesContext context) {
		String actualValue = getActualValue(resource.getId() + BEAN_SUFFIX,
				resource.getFilter(), context, component);
		return actualValue == null ? null : actualValue
				.replaceAll("\\.", "\\/");
	}

	public static String getEncoding(ResourceInfo resource,
			UIComponent component, FacesContext context) {
		return getActualValue(resource.getId() + ENCODING_SUFFIX, Utils
				.getValue(resource.getEncoding(),
						ResourceConstants.RESOURCE_ENCODING), context,
				component);
	}

	public static String getResourceValue(ResourceInfo resource,
			UIComponent component, FacesContext context) {
		return getActualValue(resource.getId(), resource.getDefaultResource(),
				context, component);
	}

	/**
	 * 
	 * @param bean
	 *            the managed bean to call writeTranslatedResource() on.
	 * @param resource
	 *            the classpath resource to load
	 * @param charEncoding
	 *            the character encoding to use
	 * @param type
	 *            the type of the resource (SCRIPT or STYLE)
	 * @return a string formated as a shale remoting URL to invoke the
	 *         translator method.
	 */
	public static String formatResource(String bean, String resource,
			String charEncoding, String type) {
		return String.format("/%s/%s?" + ResourceFilter.RESOURCE_KEY + "=%s&"
				+ ResourceFilter.CONTENT_TYPE_KEY + "=%s&"
				+ ResourceFilter.CHARSET_KEY + "=%s", bean, getFilterMethod(),
				resource, type, charEncoding);
	}

	public static String getFilterMethod() {
		return ResourceFilter.class.getMethods()[0].getName();
	}

	/**
	 * Resolves the attribute in the following sequence:
	 * <ol>
	 * <li>Checks the component for the attribute corresponding to id. If such
	 * a value is non-null, it is returned.
	 * <li>Checks the external context (web.xml) for an init parameter
	 * corresponding to id. If
	 * <li>Returns def.
	 * </ol>
	 * 
	 * @param id
	 *            the attribute id
	 * @param def
	 *            the default value
	 * @param context
	 *            the faces context
	 * @param component
	 *            the component
	 * @return see above
	 */
	public static String getActualValue(String id, String def,
			FacesContext context, UIComponent component) {
		if (Utils.isEmpty(id)) {
			return def;
		}
		String config = context.getExternalContext().getInitParameter(id);
		String value = Utils.toString(component.getAttributes().get(id), null);
		// if the attribute is null, use the init parameter, if that is null,
		// use the default
		return Utils.getValue(Utils.getValue(value, config), def);
	}

	/**
	 * Write out include links for the given resources. See {@link ResourceInfo}
	 * for more information.
	 * 
	 * @param context
	 * @param component
	 * @param resources
	 *            the {@link ResourceInfo} instances
	 * @param resourcePaths
	 *            Class path locations of the resources that have already been
	 *            included. New resource paths will be added to this set.
	 * @throws IOException
	 */
	public static void writeIncludes(FacesContext context,
			UIComponent component, Set<ResourceInfo> resources,
			Set<String> resourcePaths) throws IOException {
		for (ResourceInfo resource : resources) {
			String value = ResourceUtils.getResourceValue(resource, component,
					context);
			if (!resourcePaths.add(value)) {
				// skip resources we've already included
				continue;
			}
			if (resource != null) {
				String type = ResourceUtils.getType(resource);
				String bean = ResourceUtils.getBean(resource, component,
						context);

				String mapped;
				Mechanism mechanism;
				if (bean == null) {
					// if no filter is specified, link it as a classpath
					// resource
					mapped = value;
					mechanism = Mechanism.CLASS_RESOURCE;
				} else {
					// otherwise, create a URL to run it through the filter
					// first
					mechanism = Mechanism.DYNAMIC_RESOURCE;
					mapped = ResourceUtils.formatResource(bean, value,
							ResourceUtils.getEncoding(resource, component,
									context), type);
				}

				// let shale remoting write out the links
				if (Type.STYLE.toString().equals(type)) {
					HELPER.linkStylesheet(context, component, context
							.getResponseWriter(), mechanism, mapped);
				} else {
					HELPER.linkJavascript(context, component, context
							.getResponseWriter(), mechanism, mapped);
				}
			}
		}
	}

	/**
	 * 
	 * @param context
	 * @return the set of classpath resources that have already had a link or
	 *         script element rendered for their inclusion.
	 */
	@SuppressWarnings("unchecked")
	public static Set<String> getRenderedResources(FacesContext context) {
		Set<String> set = (Set<String>) context.getViewRoot().getAttributes()
				.get(RENDERED_RESOURCES);
		if (set == null) {
			set = new HashSet<String>();
			context.getViewRoot().getAttributes().put(RENDERED_RESOURCES, set);
		}
		return set;
	}

}
